﻿using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Text.RegularExpressions;
using DynORM.Interfaces;
using System.Xml.Serialization;
using System.Xml.Schema;
using System.Xml;
using DynORM.TypeMappingSubsystem;

namespace DynORM.ValueObjects
{

    [XmlRoot(ElementName = ForeignKeyDescriptor.ForeignKeyDescriptorTag)]
    public class ForeignKeyDescriptor : IForeignKeyDescriptor, IXmlSerializable
    {
        private RelationType relationType; //had to relax "readonly" restriction of this member in favor to init logic of xml serializer
        private string tableName; //had to relax "readonly" restriction of this member in favor to init logic of xml serializer
        private Dictionary<int, IFieldDescriptor> fieldDescriptors; //had to relax "readonly" restriction of this member in favor to init logic of xml serializer
        private string relatedTableName; //had to relax "readonly" restriction of this member in favor to init logic of xml serializer
        private Dictionary<int, IFieldDescriptor> relatedFieldDescriptors; //had to relax "readonly" restriction of this member in favor to init logic of xml serializer
        private string name; //had to relax "readonly" restriction of this member in favor to init logic of xml serializer

        //for serializer
        private ForeignKeyDescriptor()
        {
            this.relationType = RelationType.ForeignKey;
            this.fieldDescriptors = new Dictionary<int, IFieldDescriptor>();
            this.relatedFieldDescriptors = new Dictionary<int, IFieldDescriptor>();
        }

        public const string ForeignKeyDescriptorTag = "ForeignKeyDescriptor";
        public const string ForeignKeyDescriptorNameAttr = "Name";
        public const string ForeignKeyDescriptorTableNameAttr = "TableName";
        public const string ForeignKeyDescriptorFieldsTag = "ForeignKeyDescriptorFields";
        public const string ForeignKeyDescriptorRelatedTableNameAttr = "RelatedTableName";
        public const string ForeignKeyDescriptorRelatedFieldsTag = "ForeignKeyDescriptorRelatedFields";

        public ForeignKeyDescriptor(string tableName, IDictionary<int, IFieldDescriptor> fieldDescriptors, string relatedTableName, IDictionary<int, IFieldDescriptor> relatedFieldDescriptors, string name)
        {
            this.relationType = RelationType.ForeignKey;
            this.fieldDescriptors = new Dictionary<int, IFieldDescriptor>();
            this.relatedFieldDescriptors = new Dictionary<int, IFieldDescriptor>();

            this.tableName = tableName ?? String.Empty;
            foreach (KeyValuePair<int, IFieldDescriptor> fd in fieldDescriptors)
                this.fieldDescriptors.Add(fd.Key, fd.Value);
            this.relatedTableName = relatedTableName ?? String.Empty;
            foreach (KeyValuePair<int, IFieldDescriptor> rfd in relatedFieldDescriptors)
                this.relatedFieldDescriptors.Add(rfd.Key, rfd.Value);

            this.name = name;
        }

        public string Name
        {
            get
            {
                return this.name;
            }
        }

        public RelationType RelationType
        {
            get { return relationType; }
        }

        public string TableName
        {
            get { return this.tableName; }
        }

        public IDictionary<int, IFieldDescriptor> FieldDescriptors
        {
            get { return this.fieldDescriptors; }
        }

        public string RelatedTableName
        {
            get
            {
                return this.relatedTableName;
            }
        }

        public IDictionary<int, IFieldDescriptor> RelatedFieldDescriptors
        {
            get { return this.relatedFieldDescriptors; }
        }

        #region Predefined conventions
        private static string predefinedFieldNameForForeignIDs = "FRowID";

        public static string PredefinedFieldNameForForeignIDs
        {
            get { return predefinedFieldNameForForeignIDs; }
            set { predefinedFieldNameForForeignIDs = value; }
        }

        #endregion

        public CheckIntegrityResult CheckStructureIntegrity(bool usePredefinedConvention)
        {
            CheckIntegrityResult passed = new CheckIntegrityResult(true, String.Empty);

            foreach (KeyValuePair<int, IFieldDescriptor> fd in this.fieldDescriptors)
            {
                if (fd.Value.CheckStructureIntegrity(false) != passed)
                    return new CheckIntegrityResult(false, "One field failed its structure check. See inner CheckIntegrityResult.", fd.Value.CheckStructureIntegrity(false));

                if (usePredefinedConvention)
                {
                    if (!(fd.Value.FieldType == PrimaryKeyDescriptor.PredefinedTypeForIDs || fd.Value.FieldType == Types.GetCLRNullableType(PrimaryKeyDescriptor.PredefinedTypeForIDs)))
                        return new CheckIntegrityResult(false, String.Format("Field type must be {0}.", PrimaryKeyDescriptor.PredefinedTypeForIDs.Name));
                    if (!Regex.IsMatch(fd.Value.FieldName, String.Format(@"{0}_{1}_FK_{2}.*", this.TableName, this.RelatedTableName, ForeignKeyDescriptor.PredefinedFieldNameForForeignIDs)))
                        return new CheckIntegrityResult(false, String.Format("Field name must be {0}_{1}_FK_{2} (or {0}_{1}_FK_{2}_m_n in case of composite key and presence several FK that links this two tables).", this.TableName, this.RelatedTableName, ForeignKeyDescriptor.PredefinedFieldNameForForeignIDs));
                }
            }

            foreach (KeyValuePair<int, IFieldDescriptor> rfd in this.relatedFieldDescriptors)
            {
                if (rfd.Value.CheckStructureIntegrity(false) != passed)
                    return new CheckIntegrityResult(false, "One field failed its structure check. See inner CheckIntegrityResult.", rfd.Value.CheckStructureIntegrity(false));

                if (usePredefinedConvention)
                {
                    if (!(rfd.Value.FieldType == PrimaryKeyDescriptor.PredefinedTypeForIDs || rfd.Value.FieldType == Types.GetCLRNullableType(PrimaryKeyDescriptor.PredefinedTypeForIDs)))
                        return new CheckIntegrityResult(false, String.Format("Field type must be {0}.", PrimaryKeyDescriptor.PredefinedTypeForIDs.Name));
                    if (!Regex.IsMatch(rfd.Value.FieldName, String.Format(@"{0}.*", PrimaryKeyDescriptor.PredefinedFieldNameForIDs)))
                        return new CheckIntegrityResult(false, String.Format("Field name must be {0} (or {0}_n in case of composite key).", PrimaryKeyDescriptor.PredefinedFieldNameForIDs));
                }
            }

            CheckIntegrityResult checkFieldCorrRes = CheckFieldCorrespondence();
            if (checkFieldCorrRes != passed)
                return checkFieldCorrRes;

            return passed;
        }

        private CheckIntegrityResult CheckFieldCorrespondence()
        {
            CheckIntegrityResult passed = new CheckIntegrityResult(true, String.Empty);

            if (this.relatedFieldDescriptors.Count != this.fieldDescriptors.Count)
                return new CheckIntegrityResult(false, "Primary key and related foreign key differ in their structure. Keys do not contain equal number of fields.");

            for (int i = 0; i < this.fieldDescriptors.Count; i++)
            {
                //do not need an explicit cast cause do not use overloaded operator here
                IFieldDescriptor rfd = this.relatedFieldDescriptors[i];
                IFieldDescriptor fd = this.fieldDescriptors[i];
                if (!((fd.FieldType == rfd.FieldType) || (Types.IsCLRTypeNullable(fd.FieldType) && fd.FieldType == Types.GetCLRNullableType(rfd.FieldType))))
                    return new CheckIntegrityResult(false, "Primary key and related foreign key differ in their structure. Keys contain fields with not corresponding types.");
            }
            return passed;
        }

        #region Value-based equality semantics implementation
        public override bool Equals(object obj)
        {
            //ForeignKeyDescriptor that = obj as ForeignKeyDescriptor;
            //if (Object.ReferenceEquals(that, null))
            if (!(obj is ForeignKeyDescriptor))
                return false;
            ForeignKeyDescriptor that = (ForeignKeyDescriptor)obj;

            if (this.FieldDescriptors.Count() != that.FieldDescriptors.Count())
                return false;

            for (int i = 0; i < this.FieldDescriptors.Count; i++ )
            {
                //need an explicit cast cause use overloaded operator
                FieldDescriptor thisFD = (FieldDescriptor)this.FieldDescriptors[i];
                FieldDescriptor thatFD = (FieldDescriptor)that.FieldDescriptors[i];
                if (thisFD != thatFD) return false;
            }

            if (String.Compare(this.TableName, that.TableName, StringComparison.OrdinalIgnoreCase) != 0)
                return false;
            else
                return true;
        }

        public override int GetHashCode()
        {
            return this.tableName.GetHashCode();
        }

        public static bool operator ==(ForeignKeyDescriptor rd1, IRelationDescriptor rd2)
        {
            if (Object.ReferenceEquals(rd1, null))
                return false;

            return rd1.Equals(rd2);
        }

        public static bool operator !=(ForeignKeyDescriptor rd1, IRelationDescriptor rd2)
        {
            if (Object.ReferenceEquals(rd1, null))
                return false;

            return !rd1.Equals(rd2);
        }

        #endregion

        #region IXmlSerializable and auxiliary CreateFromXml member
        public XmlSchema GetSchema()
        {
            return null;
        }

        public void ReadXml(XmlReader reader)
        {
            bool related = false;
            int i = 0;
            do
            {
                switch (reader.NodeType)
                {
                    case XmlNodeType.Element:
                        {
                            if (string.Compare(reader.Name, ForeignKeyDescriptorTag, StringComparison.OrdinalIgnoreCase) == 0)
                            {
                                bool emptyElement = reader.IsEmptyElement;
                                if (reader.HasAttributes)
                                {
                                    while (reader.MoveToNextAttribute())
                                    {
                                        //if (string.Compare(reader.Name, ForeignKeyDescriptorUniqueNameAttr, StringComparison.OrdinalIgnoreCase) == 0)
                                        //    this.uniqueName = reader.Value;
                                        if (string.Compare(reader.Name, ForeignKeyDescriptorNameAttr, StringComparison.OrdinalIgnoreCase) == 0)
                                            this.name = reader.Value;
                                        else if (string.Compare(reader.Name, ForeignKeyDescriptorTableNameAttr, StringComparison.OrdinalIgnoreCase) == 0)
                                            this.tableName = reader.Value;
                                        else if (string.Compare(reader.Name, ForeignKeyDescriptorRelatedTableNameAttr, StringComparison.OrdinalIgnoreCase) == 0)
                                            this.relatedTableName = reader.Value;
                                    }
                                }
                                if (emptyElement)
                                    return;
                            }
                            else if (string.Compare(reader.Name, ForeignKeyDescriptorFieldsTag, StringComparison.OrdinalIgnoreCase) == 0)
                            {
                                related = false;
                                i = 0;
                            }
                            else if (string.Compare(reader.Name, ForeignKeyDescriptorRelatedFieldsTag, StringComparison.OrdinalIgnoreCase) == 0)
                            {
                                related = true;
                                i = 0;
                            }
                            else if (string.Compare(reader.Name, FieldDescriptor.FieldDescriptorTag, StringComparison.OrdinalIgnoreCase) == 0)
                            {
                                FieldDescriptor f = FieldDescriptor.CreateFromXml(reader);
                                if (related)
                                    this.relatedFieldDescriptors.Add(i, f);
                                else
                                    this.fieldDescriptors.Add(i, f);
                                i++;
                            }
                        } break;
                    case XmlNodeType.EndElement:
                        {
                            if (string.Compare(reader.Name, ForeignKeyDescriptorTag, StringComparison.OrdinalIgnoreCase) == 0)
                                return;
                        } break;
                    default: continue;
                }
            } while (reader.Read());
        }

        public static ForeignKeyDescriptor CreateFromXml(XmlReader reader)
        {
            //string uniqueName = String.Empty;
            string name = String.Empty;
            string tableName = String.Empty;
            string relatedTableName = String.Empty;
            Dictionary<int, IFieldDescriptor> fieldDescriptors = new Dictionary<int, IFieldDescriptor>();
            Dictionary<int, IFieldDescriptor> relatedFieldDescriptors = new Dictionary<int, IFieldDescriptor>();

            
            bool related = false;
            int i = 0;
            do
            {
                switch (reader.NodeType)
                {
                    case XmlNodeType.Element:
                        {
                            if (string.Compare(reader.Name, ForeignKeyDescriptorTag, StringComparison.OrdinalIgnoreCase) == 0)
                            {
                                bool emptyElement = reader.IsEmptyElement;
                                if (reader.HasAttributes)
                                {
                                    while (reader.MoveToNextAttribute())
                                    {
                                        //if (string.Compare(reader.Name, ForeignKeyDescriptorUniqueNameAttr, StringComparison.OrdinalIgnoreCase) == 0)
                                        //    uniqueName = reader.Value;
                                        if (string.Compare(reader.Name, ForeignKeyDescriptorNameAttr, StringComparison.OrdinalIgnoreCase) == 0)
                                            name = reader.Value;
                                        else if (string.Compare(reader.Name, ForeignKeyDescriptorTableNameAttr, StringComparison.OrdinalIgnoreCase) == 0)
                                            tableName = reader.Value;
                                        else if (string.Compare(reader.Name, ForeignKeyDescriptorRelatedTableNameAttr, StringComparison.OrdinalIgnoreCase) == 0)
                                            relatedTableName = reader.Value;
                                    }
                                }
                                if (emptyElement)
                                    return new ForeignKeyDescriptor(tableName, fieldDescriptors, relatedTableName, relatedFieldDescriptors, name);
                            }
                            else if (string.Compare(reader.Name, ForeignKeyDescriptorFieldsTag, StringComparison.OrdinalIgnoreCase) == 0)
                            {
                                related = false;
                                i = 0;
                            }
                            else if (string.Compare(reader.Name, ForeignKeyDescriptorRelatedFieldsTag, StringComparison.OrdinalIgnoreCase) == 0)
                            {
                                related = true;
                                i = 0;
                            }
                            else if (string.Compare(reader.Name, FieldDescriptor.FieldDescriptorTag, StringComparison.OrdinalIgnoreCase) == 0)
                            {
                                FieldDescriptor f = FieldDescriptor.CreateFromXml(reader);
                                if (related)
                                    relatedFieldDescriptors.Add(i, f);
                                else
                                    fieldDescriptors.Add(i, f);
                                i++;
                            }
                        } break;
                    case XmlNodeType.EndElement:
                        {
                            if (string.Compare(reader.Name, ForeignKeyDescriptorTag, StringComparison.OrdinalIgnoreCase) == 0)
                                return new ForeignKeyDescriptor(tableName, fieldDescriptors, relatedTableName, relatedFieldDescriptors, name);
                        } break;
                    default: continue;
                }
            } while (reader.Read());
            return new ForeignKeyDescriptor(tableName, fieldDescriptors, relatedTableName, relatedFieldDescriptors, name);
        }

        public void WriteXml(XmlWriter writer)
        {
            //writer.WriteAttributeString(ForeignKeyDescriptorUniqueNameAttr, this.UniqueName);
            writer.WriteAttributeString(ForeignKeyDescriptorNameAttr, this.Name);
            writer.WriteAttributeString(ForeignKeyDescriptorTableNameAttr, this.TableName);
            writer.WriteAttributeString(ForeignKeyDescriptorRelatedTableNameAttr, this.RelatedTableName);
            if (this.FieldDescriptors.Count() > 0)
            {
                writer.WriteStartElement(ForeignKeyDescriptorFieldsTag);
                foreach (KeyValuePair<int, IFieldDescriptor> f in this.FieldDescriptors.OrderBy<KeyValuePair<int, IFieldDescriptor>, int>(kv => { return kv.Key; }))
                {
                    writer.WriteStartElement(FieldDescriptor.FieldDescriptorTag);
                    ((FieldDescriptor)f.Value).WriteXml(writer);
                    writer.WriteEndElement();
                }
                writer.WriteEndElement();
            }
            if (this.RelatedFieldDescriptors.Count() > 0)
            {
                writer.WriteStartElement(ForeignKeyDescriptorRelatedFieldsTag);
                foreach (KeyValuePair<int, IFieldDescriptor> f in this.RelatedFieldDescriptors.OrderBy(kv => kv.Key))
                {
                    writer.WriteStartElement(FieldDescriptor.FieldDescriptorTag);
                    ((FieldDescriptor)f.Value).WriteXml(writer);
                    writer.WriteEndElement();
                }
                writer.WriteEndElement();
            }
        }
        #endregion    

    }

}
